<?php

class CustomRepository extends Repository
{

  protected $params;

  /**
   *
   * @var array
   */
  protected static $instances = array();

  /**
   *
   * @param string $name
   * @return AbstractRepository
   */
  public static function getInstance($name)
  {
    if (empty(self::$instances[$name])) {
      self::$instances[$name] = new self($name);
    }

    return self::$instances[$name];
  }

  /**
   *
   * @param string $name
   * @return void
   */
  protected function __construct($name)
  {
    $this->params = Domain::getInstance()->getRepository($name);
  }

  public function save(&$data, $params = array())
  {
    $def = $this->getRoot();
    $this->_save($data, $def, $params);
  }

  public function _save(&$data, $def, $params = array())
  {
    $params = array_merge(array('depth' => - 1), $this->params, ( array ) $params);

    $depth = sy_get_param($params, 'depth');

    if (! empty($params['include']) && $depth != 0) {
      try {
        $def = Domain::toAggregate($def);
        foreach ($def->getAssociations() as $assoc_name => $assoc) {
          if ($this->includes($params, $assoc_name)) {
            $def->getAssociation($assoc_name)->getDAO()->beforeSave($data, $this);
          }
        }
      }
      catch (DomainTypeConversionException $e) {}
      catch (AssociationNotFoundException $e) {}
    }

    $def = Domain::toEntity($def);

    $id = sy_get_param($data, $def->getPk());
    if (empty($id)) {
      $this->_insert($def, $data);
    }
    else {
      $this->_update($def, $data);
    }

    if (! empty($params['include']) && $depth != 0) {
      try {
        $def = Domain::toAggregate($def);
        foreach ($def->getAssociations() as $assoc_name => $assoc) {
          if ($this->includes($params, $assoc_name)) {
            $def->getAssociation($assoc_name)->getDAO()->afterSave($data, $this);
          }
        }
      }
      catch (DomainTypeConversionException $e) {}
      catch (AssociationNotFoundException $e) {}
    }
  }

  protected function includes($params, $name)
  {
    if (empty($params['include'])) {
      return false;
    }

    foreach ($params['include'] as $inc) {
      if ($inc == '*') {
        return true;
      }

      if ($inc == $name) {
        return true;
      }

      if (is_array($inc) && $inc[0] == $name) {
        return true;
      }
    }

    return false;
  }

  public function findCount($params = array())
  {
    $params = array_merge(array('depth' => - 1), $this->params, ( array ) $params);

    $def = Domain::toEntity($this->getRoot());

    $table = $def->getTable();
    $pk = $def->getPk();

    $query = DAO::getInstance()->query()->from($table)->select('1');

    $this->setOrderBy($query, $params);
    $this->setJoins($query, $params);
    $this->setOffset($query, $params);
    $this->setWhere($query, $params);

    $result = $query->execute(sy_get_param($params, 'data'));

    $data = $result->numRows();

    return $data;
  }

  public function find($params = array())
  {
    $params = array_merge(array('depth' => - 1), $this->params, ( array ) $params);

    $def = Domain::toEntity($this->getRoot());

    $table = $def->getTable();

    if (isset($params['sql'])) {
      $query = DAO::getInstance()->query($params['sql']);
    }
    else {
      $query = DAO::getInstance()->query()->from($table);

      $this->setFields($query, $params);
      $this->setJoins($query, $params);
      $this->setOrderBy($query, $params);
      $this->setOffset($query, $params);
      $this->setWhere($query, $params);
    }

    $result = $query->execute(sy_get_param($params, 'data'));

    $data = $result->fetchRow();

    if (empty($data)) {
      return null;
    }

    $depth = sy_get_param($params, 'depth');

    if (! empty($params['include']) && $depth != 0) {
      $this->intr($def, $data, $params['include']);
    }

    $mappingDepth = sy_get_param($params, 'mappingDepth', 0);

    if ($mappingDepth == 0) {
      return $data;
    }

    return AbstractFactory::factory($def, $data, $mappingDepth);
  }

  public function findAll($params = array())
  {
    $params = array_merge(array('depth' => - 1), $this->params, ( array ) $params);

    $def = Domain::toEntity($this->getRoot());

    $table = $def->getTable();

    if (isset($params['sql'])) {
      $query = DAO::getInstance()->query($params['sql']);
    }
    else {
      $query = DAO::getInstance()->query()->from($table);

      $this->setFields($query, $params);
      $this->setJoins($query, $params);
      $this->setOrderBy($query, $params);
      $this->setWhere($query, $params);
    }

    $this->setLimit($query, $params);
    $this->setOffset($query, $params);
    $this->setWhere($query, $params);

    $result = $query->execute(sy_get_param($params, 'data'));

    $data = $result->fetchAll();

    if (empty($data)) {
      return null;
    }

    $depth = sy_get_param($params, 'depth');

    if (! empty($params['include']) && $depth != 0) {
      $this->intr($def, $data, $params['include']);
    }

    $mappingDepth = sy_get_param($params, 'mappingDepth', 0);

    if ($mappingDepth == 0) {
      return $data;
    }

    return AbstractFactory::factoryCollection($def, $data, $mappingDepth);
  }

  protected function getIncludes($params)
  {
    $_params = array();
    foreach (( array ) $params as $name => $param) {
      if (is_array($param)) {
        $_params[$param[0]] = sy_get_param($param, 1, array());
      }
      else {
        $_params[$param] = $param;
      }
    }
    return $_params;
  }

  protected function intr($def, &$data, $params)
  {
    $params = ( array ) $params;

    $depth = sy_get_param($params, 'depth', - 1);

    if ($depth <= - 255) {
      throw new Exception("Maximum recursions reached");
    }

    if (empty($data)) {
      return;
    }

    $params = $this->getIncludes($params);

    try {
      $def = Domain::toAggregate($def);

      foreach ($def->getAssociations() as $assoc_name => $assoc) {
        if (! isset($params[$assoc_name]) && ! isset($params['*'])) {
          continue;
        }

        $assoc = $def->getAssociation($assoc_name);

        $assoc->getDAO()->find($data, isset($params[$assoc_name]) ? $params[$assoc_name] : null);

        $k = $def->getPk();

        $params['depth'] = $depth - 1;

        if (isset($params[$assoc_name]['include']) && $depth != 0) {
          if (isset($data[$k])) {
            $this->intr($assoc->getTarget(), $data[$assoc_name], $params[$assoc_name]['include']);
          }
          else {
            $m = array();

            foreach ($data as &$row) {
              if (empty($row[$assoc_name])) {
                continue;
              }
              elseif (isset($row[$assoc_name][$k])) {
                $m[] = &$row;
              }
              elseif (isset($row[$assoc_name][$assoc->getForeignKeyField()->getName()])) {
                $m[] = &$row[$assoc_name];
              }
              else {
                foreach ($row[$assoc_name] as &$child) {
                  $m[] = &$child;
                }
              }
            }

            $this->intr($assoc->getTarget(), $m, $params[$assoc_name]['include']);
          }
        }
      }
    }
    catch (DomainTypeConversionException $e) {}
  }

  protected function setFields(IQueryObject $query, array $params)
  {
    if (isset($params['fields'])) {
      $query->select($params['fields']);
    }
    else {
      $query->select(Repository::collectFields($this->getRoot()));
    }
  }

  protected function setJoins(IQueryObject $query, array $params)
  {
    if (isset($params['joins'])) {
      foreach ($params['joins'] as $join) {
        $query->join(sy_get_param($join, 0) . ' ON ' . sy_get_param($join, 1), sy_get_param($join, 2));
      }
    }
  }

  protected function setOrderBy(IQueryObject $query, array $params)
  {
    if (isset($params['orderBy'])) {
      $query->orderBy($params['orderBy']);
    }
  }

  protected function setLimit(IQueryObject $query, array $params)
  {
    if (isset($params['limit'])) {
      $query->limit($params['limit']);
    }
  }

  protected function setOffset(IQueryObject $query, array $params)
  {
    if (isset($params['offset'])) {
      $query->offset($params['offset']);
    }
  }

  protected function setWhere(IQueryObject $query, array $params)
  {
    if (isset($params['where'])) {
      if (is_array($params['where'])) {
        foreach ($params['where'] as $where) {
          $query->where($where);
        }
      }
      else {
        $query->where($params['where']);
      }
    }
  }

  /**
   *
   * @param IDomObjDefinition $def
   * @param array $data
   * @return array
   */
  protected function _insert($def, &$data)
  {
    $def = Domain::toEntity($def);

    $_data = Repository::collectData($def, $data);

    $table = $def->getTable();
    $pk = $def->getPk();

    $dao = DAO::getInstance();

    $sql = "INSERT INTO $table " . QueryObject::buildInsert($_data, '?');
    $query = $dao->query($sql);

    $result = $query->execute(array_values($_data));

    if (is_object($data)) {
      $data->$pk = $dao->lastInsertId();
    }
    else {
      $data[$pk] = $dao->lastInsertId();
    }
  }

  /**
   *
   * @param IDomObjDefinition $def
   * @param array $data
   * @return void
   */
  protected function _update($def, &$data)
  {
    $def = Domain::toEntity($def);

    $_data = Repository::collectData($def, $data);

    $table = $def->getTable();
    $pk = $def->getPk();
    $id = $_data[$pk];

    unset($_data[$pk]);

    $dao = DAO::getInstance();

    $query = $dao->query("UPDATE $table SET " . QueryObject::buildUpdate($_data, '?') . " WHERE $pk = ?");

    $_data[] = $id;

    $result = $query->execute(array_values($_data));
  }

  /**
   *
   * @return IDomObj
   */
  protected function getRoot()
  {
    return Domain::getInstance()->getObject($this->params['root']);
  }

}

?>